package net.qiqbframework.messaging.task;


import lombok.extern.slf4j.Slf4j;
import net.qiqbframework.common.Assert;
import net.qiqbframework.messaging.Message;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.EnumMap;
import java.util.LinkedList;
import java.util.function.Consumer;
import java.util.function.Function;


@Slf4j
public class MessageProcessingContext<T extends Message<?>> {

    private static final Deque EMPTY = new LinkedList<>();

    private final EnumMap<TaskPhase, Deque<Consumer<MessageTask<T>>>> handlers = new EnumMap<>(TaskPhase.class);
    private T message;
    private ExecutionResult executionResult;

    /**
     * Creates a new processing context for the given {@code message}.
     *
     * @param message The Message that is to be processed.
     */
    public MessageProcessingContext(T message) {
        this.message = message;
    }

    /**
     * Invoke the handlers in this collection attached to the given {@code phase}.
     *
     * @param MessageWork The Unit of Work that is changing its phase
     * @param phase       The phase for which attached handlers should be invoked
     */
    @SuppressWarnings("unchecked")
    public void notifyHandlers(MessageTask<T> MessageWork, TaskPhase phase) {
        if (log.isDebugEnabled()) {
            log.debug("Notifying handlers for phase {}", phase.toString());
        }
        Deque<Consumer<MessageTask<T>>> l = handlers.getOrDefault(phase, EMPTY);
        while (!l.isEmpty()) {
            try {
                l.remove().accept(MessageWork);
            } catch (Exception e) {
                if (phase.isSuppressHandlerErrors()) {
                    log.info("An error occurred while executing a lifecycle phase handler for phase {}", phase, e);
                } else {
                    throw e;
                }
            }
        }
    }

    /**
     * Adds a handler to the collection. Note that the order in which you register the handlers determines the order
     * in which they will be handled during the various stages of a unit of work.
     *
     * @param phase   The phase of the unit of work to attach the handler to
     * @param handler The handler to invoke in the given phase
     */
    public void addHandler(TaskPhase phase, Consumer<MessageTask<T>> handler) {
        if (log.isDebugEnabled()) {
            log.debug("Adding handler {} for phase {}", handler.getClass().getName(), phase.toString());
        }
        final Deque<Consumer<MessageTask<T>>> consumers = handlers.computeIfAbsent(phase, p -> new ArrayDeque<>());
        if (phase.isReverseCallbackOrder()) {
            consumers.addFirst(handler);
        } else {
            consumers.add(handler);
        }
    }

    /**
     * Set the execution result of processing the current {@link #getMessage() Message}. In case this context has a
     * previously set ExecutionResult, setting a new result is only allowed if the new result is an exception result.
     * <p/>
     * In case the previously set result is also an exception result, the exception in the new execution result is
     * added to the original exception as a suppressed exception.
     *
     * @param executionResult the ExecutionResult of the currently handled Message
     */
    @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
    public void setExecutionResult(ExecutionResult executionResult) {
        /*Assert.state(this.executionResult == null || executionResult.isExceptionResult(),
                () -> String.format("Cannot change execution result [%s] to [%s] for message [%s].",
                        this.executionResult, executionResult, message));*/
        if (this.executionResult != null && this.executionResult.isExceptionResult()) {
            this.executionResult.getExceptionResult().addSuppressed(executionResult.getExceptionResult());
        } else {
            this.executionResult = executionResult;
        }
    }

    /**
     * Get the Message that is being processed in this context.
     *
     * @return the Message that is being processed
     */
    public T getMessage() {
        return message;
    }


    public ExecutionResult getExecutionResult() {
        return executionResult;
    }

    /**
     * Transform the Message being processed using the given operator.
     *
     * @param transformOperator The transform operator to apply to the stored message
     */
    public void transformMessage(Function<T, ? extends Message<?>> transformOperator) {
        message = (T) transformOperator.apply(message);
    }

    /**
     * Reset the processing context. This clears the execution result and map with registered handlers, and replaces
     * the current Message with the given {@code message}.
     *
     * @param message The new message that is being processed
     */
    public void reset(T message) {
        this.message = message;
        handlers.clear();
        executionResult = null;
    }
}
